package app;

import java.math.BigDecimal;
import java.util.*;

enum ImpurityMeasure {
	Gini, Entropy
};

public class DataSet {
	private AttributeSet attributeSet;
	private SymbolicAttribute problemClass;
	private Vector<Instance> instanceSet;
	private ImpurityMeasure impurityMeasure = ImpurityMeasure.Gini;
	private boolean flagLaplace;

	public DataSet() { }

	public DataSet(AttributeSet attributeSet) {
		if (attributeSet == null)
			throw new IllegalArgumentException("Invalid argument");

		this.attributeSet = attributeSet;
		// TODO: test if the problem class is a SymbolicAttribute
		this.problemClass = (SymbolicAttribute) attributeSet.get(attributeSet.size() - 1);

		instanceSet = new Vector<Instance>();
	}

	public void setImpurityMeasure(ImpurityMeasure impurityMeasure) {
		this.impurityMeasure = impurityMeasure;
	}

	public void add(Instance instance) {
		if (instance == null || instance.noAttributes() != attributeSet.size())
			throw new IllegalArgumentException("Invalid instance");

		instanceSet.add(instance);
	}
	
	public void sortInstanceSet(NumericAttribute numericAttribute) {
		final int attributeIndex = attributeSet.indexOf(numericAttribute);
		
		Collections.sort(instanceSet, new Comparator<Instance>() {

			public int compare(Instance i1, Instance i2) {
				double value1 = ((NumericValue) i1.getValue(attributeIndex)).getValue();
				double value2 = ((NumericValue) i2.getValue(attributeIndex)).getValue();
				if (value1 >= value2)
					return 1;
				return 0;
			}
		});
/*		
		for (Instance instance : instanceSet) {
			System.out.println(instance);
		} 
*/
	}

	public Attribute findBestSplit() {
		int noAttributes = attributeSet.size() - 1;
		double splitGini = Double.MAX_VALUE;
		Attribute splitAttribute = null;

		for (int i = 0; i < noAttributes; i++) {
			Attribute attribute = attributeSet.get(i);
			double gini = 0;
			if (attribute instanceof SymbolicAttribute) {
				gini = findGainValue(attribute);
			} else if (attribute instanceof NumericAttribute) {
				gini = findGainValueContinuousNew((NumericAttribute) attribute);
			}
			if (splitGini > gini) {
				splitGini = gini;
				splitAttribute = attributeSet.get(i);
			}
		}

		if (splitAttribute != null) {
			System.out.println("Split attribute = " + splitAttribute.getName());
			System.out.println();
		}

		return splitAttribute;
	}

	// TODO: use other methods instead of Gini
	// TODO: optimize: calculate Gini only where the value of the class changes
	// TODO: reuse the Gini function
	public double findGainValueContinuousNew(NumericAttribute attribute) {
		// the position of the attribute in the instance
		int attributeIndex = attributeSet.indexOf((Attribute) attribute);
		// the position of the class value in the instance
		int classIndex = attributeSet.size() - 1;
		int classNoValues = problemClass.noValues();
		int noInstances = instanceSet.size();
		// lower and higher partition occurrences
		int[] lowerOccurrences = new int[classNoValues];
		int[] higherOccurrences = new int[classNoValues];
		// partitions' values and gini index
		double[] partitionsValues = new double[noInstances + 1]; 
		double bestGini = Double.MAX_VALUE;
		
		// check if we determined correctly the attribute index
//		System.out.println("Attribute index: " + attributeIndex);

		sortInstanceSet(attribute);

		// find the values for lower and higher class values occurrences, 
		// for the first partition
		for (Instance instance : instanceSet) {
			SymbolicValue classValue = (SymbolicValue) instance.getValue(classIndex);
			int intClassValue = classValue.hashCode();
			higherOccurrences[intClassValue]++;
		}
/*		
		// print higher class values occurrences for the first value for attribute
		System.out.println("Higher class values occurrences for the first value for attribute: " + attribute);
		for(int i = 0; i < higherOccurrences.length; i++) 
			System.out.println(i + " = " + higherOccurrences[i]);
*/		
		// create partitions for attribute values
		for (int i = 0; i < noInstances + 1; i++) {
			if (i == 0) {
				// compute the first partition value
				NumericValue firstAttributeValue = (NumericValue) instanceSet.get(i).getValue(attributeIndex);
				partitionsValues[0] = 0.95 * firstAttributeValue.getValue();
			} else {
				if (i == noInstances) {
					// compute the last partition value
					NumericValue lastAttributeValue = (NumericValue) instanceSet.get(i-1).getValue(attributeIndex);
					partitionsValues[i] = 1.05 * lastAttributeValue.getValue();
				} else {
					// compute the other partition values
					NumericValue currentAttributeValue = (NumericValue) instanceSet.get(i).getValue(attributeIndex);
					NumericValue previousAttributeValue = (NumericValue) instanceSet.get(i-1).getValue(attributeIndex);
					partitionsValues[i] = (currentAttributeValue.getValue() + previousAttributeValue.getValue()) / 2;
				}
				
				// determine the class value
				SymbolicValue classValue = (SymbolicValue) instanceSet.get(i-1).getValue(classIndex);
				int intClassValue = classValue.hashCode();
				// update the occurrences
				lowerOccurrences[intClassValue]++;
				higherOccurrences[intClassValue]--;
			}
			
			// calculate the sum for lower and higher occurrences
			int lowerOccurrencesSum = 0, higherOccurrencesSum = 0;
			for (int j = 0; j < classNoValues; j++) {
				lowerOccurrencesSum += lowerOccurrences[j];
				higherOccurrencesSum += higherOccurrences[j];
			}
			// calculate the gini for lower and higher occurrences
			double totalLowerGini = 1, totalHigherGini = 1;
			for (int j = 0; j < classNoValues; j++) {
				double lowerGini = 0, higherGini = 0;
				lowerGini = Math.pow((double) lowerOccurrences[j] / lowerOccurrencesSum, 2);
				if (!Double.isNaN(lowerGini))
					totalLowerGini -= lowerGini;
				higherGini = Math.pow((double) higherOccurrences[j] / higherOccurrencesSum, 2);
				if (!Double.isNaN(higherGini))
					totalHigherGini -= higherGini;
			}
			// calculate the gain value
			double totalGini = totalLowerGini * lowerOccurrencesSum / instanceSet.size() + totalHigherGini * higherOccurrencesSum / instanceSet.size();
			totalGini = round(totalGini);
			
			// check for a new best gini
			if (bestGini > totalGini) {
				bestGini = totalGini;
				attribute.setSplitValue(new NumericValue(partitionsValues[i]));
			}
		
			// print the gain value
//			System.out.println("Partition= " + partitionsValues[i] + " gini= " + totalGini);
		}
		
		System.out.println("Gini " + attribute + " = " + bestGini + " split value= " + attribute.getSplitValue());
		
		// print the partition values
//		System.out.println("Partition values for attribute " + attribute + ": ");
/*
		System.out.print("Partitions " + attribute + ": ");
		for (int i = 0; i < noInstances + 1; i++)
			System.out.print(partitionsValues[i] + ", ");
		System.out.println();
*/		
		// round the result
		return bestGini;
	}
	
	public double findGainValue(Attribute attribute) {
		double gainValue = 0;

		int attributeIndex = attributeSet.indexOf(attribute);
		int attributeNoValues = ((SymbolicAttribute) attribute).noValues();
		int classNoValues = problemClass.noValues();
		int[][] occurrences = new int[attributeNoValues][classNoValues + 1];

		// find value occurrences
		for (Instance instance : instanceSet) {
			SymbolicValue attributeValue = (SymbolicValue) instance.getValue(attributeIndex);
			int intAttributeValue = attributeValue.hashCode();
			SymbolicValue classValue = (SymbolicValue) instance.getValue(instance.noAttributes() - 1);
			int intClassValue = classValue.hashCode();

			occurrences[intAttributeValue][intClassValue]++;
			occurrences[intAttributeValue][classNoValues]++;
		}

		switch (impurityMeasure) {
		case Gini:
			gainValue = calculateGini(occurrences);
			System.out.println("Gini " + attribute.getName() + " = " + gainValue);
			break;
		case Entropy:
			gainValue = calculateEntropy(occurrences);
			System.out.println("Entropy " + attribute.getName() + " = " + gainValue);
			break;
		}

		return gainValue;
	}

	public double calculateGini(int[][] occurrences) {
		double totalGini = 0;

		int attributeNoValues = occurrences.length;
		for (int i = 0; i < attributeNoValues; i++) {
			double gini = 1;
			int classNoValues = occurrences[i].length;
			for (int j = 0; j < classNoValues - 1; j++) {
				double valueGini = Math.pow((double) occurrences[i][j] / occurrences[i][classNoValues - 1], 2);
				if (!Double.isNaN(valueGini))
					gini -= valueGini;
			}
			totalGini += (double) occurrences[i][classNoValues - 1] / instanceSet.size() * gini;
		}

		return round(totalGini);
	}

	public double calculateEntropy(int[][] occurrences) {
		double totalEntropy = 0;

		int attributeNoValues = occurrences.length;
		for (int i = 0; i < attributeNoValues; i++) {
			double entropy = 0;
			int classNoValues = occurrences[i].length;
			for (int j = 0; j < classNoValues - 1; j++) {
				double v = (double) occurrences[i][j] / occurrences[i][classNoValues - 1];
				if (!Double.isNaN(v) && v != 0)
					entropy += v * Math.log(v) / Math.log(2);
			}
			totalEntropy += (double) occurrences[i][classNoValues - 1] / instanceSet.size() * entropy;
		}

		return round(-1 * totalEntropy);
	}
	
	public DataSet[] split(Attribute splitAttribute) {
		if (splitAttribute instanceof SymbolicAttribute)
			return splitSymbolicAttribute((SymbolicAttribute) splitAttribute);
		
		if (splitAttribute instanceof NumericAttribute)
			return splitNumericAttribute((NumericAttribute) splitAttribute);
		
		return null;
	}

	public DataSet[] splitSymbolicAttribute(Attribute splitAttribute) {
		SymbolicAttribute attribute = (SymbolicAttribute) splitAttribute;
		int attributeNoValues = attribute.noValues();
		int attributeIndex = attributeSet.indexOf(attribute);
		DataSet[] dataSetArray = new DataSet[attributeNoValues];

		for (int i = 0; i < attributeNoValues; i++) {
//			System.out.println(attribute + " = " + attribute.getValue(i));
			dataSetArray[i] = new DataSet(new AttributeSet(attributeSet, attributeIndex));
			dataSetArray[i].setImpurityMeasure(impurityMeasure);

			for (Instance instance : instanceSet) {
				SymbolicValue attributeValue = (SymbolicValue) instance.getValue(attributeIndex);
				if (attributeValue.hashCode() == i) {
					dataSetArray[i].add(new Instance(instance, attributeIndex));
				}
			}
//			System.out.println(dataSetArray[i]);
		}
		
		return dataSetArray;
	}

	public DataSet[] splitNumericAttribute(NumericAttribute splitAttribute) {
		// TODO: storing the split partition value doesn't feel right
		double splitValue = splitAttribute.getSplitValue().getValue();
		int attributeIndex = attributeSet.indexOf(splitAttribute);
		DataSet[] dataSetArray = new DataSet[2];

		for (int i = 0; i < 2; i++) {
			dataSetArray[i] = new DataSet(new AttributeSet(attributeSet, attributeIndex));
			dataSetArray[i].setImpurityMeasure(impurityMeasure);
		}
		for (Instance instance : instanceSet) {
			NumericValue attributeValue = (NumericValue) instance.getValue(attributeIndex);
			if (attributeValue.getValue() < splitValue) {
				dataSetArray[0].add(new Instance(instance, attributeIndex));
			} else
				dataSetArray[1].add(new Instance(instance, attributeIndex));
			System.out.print("");
		}
		// print the 2 resulting data sets
/*		System.out.println("For " + splitAttribute + " < " + splitAttribute.getSplitValue() + ": ");
		System.out.println(dataSetArray[0]);
		System.out.println("For " + splitAttribute + " >= " + splitAttribute.getSplitValue() + ": ");
		System.out.println(dataSetArray[1]);
*/
		
		return dataSetArray;
	}

	// TODO: calculate this add()
	public SymbolicValue findDefaultValue() {
		int classNoValues = problemClass.noValues();
		int[] classValues = new int[classNoValues];
		int noAttributes = attributeSet.size();
		int indexDefaultValue;

		for (int i = 0; i < classNoValues; i++)
			classValues[i] = 0;

		for (Instance instance : instanceSet) {
			SymbolicValue classValue = (SymbolicValue) instance.getValue(noAttributes - 1);
			int intClassValue = classValue.hashCode();
			classValues[intClassValue]++;
		}

		indexDefaultValue = 0;
		for (int i = 0; i < classNoValues; i++)
			if (classValues[indexDefaultValue] < classValues[i])
				indexDefaultValue = i;

		return problemClass.getValue(indexDefaultValue);
	}

	// TODO: verify that splitAttribute is part of attributeSet
	public SymbolicValue getHomogenousValue() {
		SymbolicValue homogenousValue = null;

		for (Instance instance : instanceSet) {
			SymbolicValue classValue = (SymbolicValue) instance.getValue(instance.noAttributes() - 1);
			if (homogenousValue == null)
				homogenousValue = classValue;
			else if (homogenousValue.equals(classValue) == false)
				return null;
		}

		return homogenousValue;
	}
	
	private double calculateDistance(Instance instance, Instance newInstance) {
//		System.out.println("\nInstance:" + instance + "new instance: " + newInstance);
		List<AttributeValue> values = instance.getValues();
		List<AttributeValue> newValues = newInstance.getValues();
		double[] distances = new double[values.size() - 1];
		double euclideanDistance = 0;
		double sum = 0;

		for (int i = 0; i < values.size() - 1; i++) {
			AttributeValue value = values.get(i);
			AttributeValue newValue = newValues.get(i);

			// calculate distances for symbolic values
			if (value instanceof SymbolicValue) {
				if (value.equals(newValue)) {
					distances[i] = 0;
				} else {
					distances[i] = 1;
				}
			}

			// calculate distances for numeric values
			else if (value instanceof NumericValue) {
				NumericAttribute numAttribute = (NumericAttribute) attributeSet.get(i);

				// we normalize the values for both the new and old instance
				double doubleValue = ((NumericValue) value).getValue();
				double doubleNewValue = ((NumericValue) newValue).getValue();
				double minValue = numAttribute.getMinValue().getValue();
				double maxValue = numAttribute.getMaxValue().getValue();
				double normalizedValue = (doubleValue - minValue) / (maxValue - minValue);
				double normalizedNewValue = (doubleNewValue - minValue) / (maxValue - minValue);

				distances[i] = Math.abs(normalizedNewValue - normalizedValue);
			}
			sum += Math.pow(distances[i], 2);
			// System.out.println("Value: " + value + " new Value: " + newValue
			// + " distance: " +distances[i]);
			// System.out.println("Sum = " + sum);
		}
		euclideanDistance = Math.sqrt(sum);
		// System.out.println("Euclidean distance between instances " + instance
		// + " and " + newInstance + ": " + euclideanDistance );
		return euclideanDistance;
	}

	// TODO: see when the result is 'Infinity'
	private double[] calculateWeights(List<Double> remainingDistances) {
		int size = remainingDistances.size();
		double[] weights = new double[size];
		for (int i = 0; i < size; i++) {
			weights[i] = 1 / Math.pow((remainingDistances.get(i)), 2);
//			System.out.println("distance: " + remainingDistances.get(i) + " w = " + weights[i]);
		}

		return weights;
	}

	// TODO: if distance = 0 then class(newInstance) = class(instance)
	public void kNearestNeighbour(Instance newInstance, int k, boolean flag) {
		final List<Double> euclideanDistances = new ArrayList<Double>();
		List<Double> remainingDistances = new ArrayList<Double>();
		Vector<Instance> remainingInstances = new Vector<Instance>();
		SymbolicValue newClassValue = null;

		// calculate distances between the new instance and the other instances
		for (Instance instance : instanceSet) {
			double distance = calculateDistance(instance, newInstance);
			euclideanDistances.add(distance);
		}

		// sort instances according to distances
		Collections.sort(instanceSet, new Comparator<Instance>() {

			public int compare(Instance instance1, Instance instance2) {
				int index1 = instanceSet.indexOf(instance1);
				int index2 = instanceSet.indexOf(instance2);
				if (euclideanDistances.get(index1) > euclideanDistances.get(index2))
					return 1;
				return 0;
			}
		});

//		System.out.println(instances);

		Collections.sort(euclideanDistances);

		if (flag == true) {
			// get the k nearest neighbors with the k smallest distances
			for (int i = 0; i < k; i++) {
				remainingDistances.add(euclideanDistances.get(i));
				remainingInstances.add(instanceSet.get(i));
			}
			System.out.println("Cele mai mici k distante: ");
			System.out.println(remainingDistances);
			System.out.println("Cele mai apropiate k instante: ");
			System.out.println(remainingInstances);

			// calculate weights for k smallest distances
			double[] weights = calculateWeights(remainingDistances);

			/* classify the new instance - find out the class value for it */

			// first way - calculate sums
			double[] sums = new double[getProblemClass().noValues()];
			for (int i = 0; i < remainingInstances.size(); i++) {
				Instance remInstance = remainingInstances.get(i);
				SymbolicValue classValue = (SymbolicValue) remInstance.getValue(remInstance.noAttributes() - 1);
				int intClassValue = classValue.hashCode();
				sums[intClassValue] += weights[i];
			}
			double maxSum = Double.MIN_VALUE;
			int indexForMax = 0;
			for (int i = 0; i < sums.length; i++) {
				if (sums[i] > maxSum) {
					maxSum = sums[i];
					indexForMax = i;
				}
			}
			newClassValue = getProblemClass().getValue(indexForMax);
			System.out.println("Clasa pentru noua instanta: " + newClassValue);
		} else if (flag == false) {

			// second way - choose minimum distance
			double minDistance = euclideanDistances.get(0);
			Instance minDistanceInstance = instanceSet.get(euclideanDistances.indexOf(minDistance));
			newClassValue = (SymbolicValue) minDistanceInstance.getValue(minDistanceInstance.getValues().size() - 1);
			System.out.println("Clasa pentru noua instanta: " + newClassValue);
		}
	}
	
	// TODO: check this function - the formula for the multiplication 
	public void naiveBayes(Instance newInstance) {
		List<AttributeValue> newValues = newInstance.getValues();
		List<SymbolicValue> classValues = problemClass.getValues();
		double pNewInstance[] = new double[classValues.size()];
		
		for(int i = 0; i < classValues.size(); i++) {
			pNewInstance[i] = 1;
		}
		
		for (SymbolicValue classValue: classValues) {
			double pClass = pClass(classValue);
			int intClassValue = classValue.hashCode();
			pNewInstance[intClassValue] *= pClass;
			for(int i = 0; i < newValues.size() - 1; i++) {
				AttributeValue value = newValues.get(i);
				if(value instanceof SymbolicValue) {
					SymbolicAttribute attribute = (SymbolicAttribute) attributeSet.get(i);
					pNewInstance[intClassValue] *= pSymbolicAttribute(attribute, (SymbolicValue)value, classValue);
				}
				else if(value instanceof NumericValue) {
					NumericAttribute attribute = (NumericAttribute) attributeSet.get(i);
					pNewInstance[intClassValue] *= pNumericAttribute(attribute, (NumericValue)value, classValue);
				}
			}
		}
		
		double argMax = Double.MIN_VALUE;
		int indexForMax = 0;
		System.out.println("\nProbabilitatile pentru valorile clasei: ");
		for(int i = 0; i < classValues.size(); i++) {
			System.out.println(pNewInstance[i] + " pentru valoarea: " + classValues.get(i));
			if(pNewInstance[i] > argMax) {
				argMax = pNewInstance[i];
				indexForMax = i;
			}
		}
		
		System.out.println("Clasa pentru noua instanta: " + classValues.get(indexForMax));
	}
	
	private double pClass(SymbolicValue classValue) {
		int noInstances = 0;
		
		for (Instance instance : instanceSet) {
			if(classValue.equals(instance.getValue(instance.noAttributes() - 1))) {
				noInstances++ ;
			}
		}
		System.out.println("Probabilitate clasa: " + (double) noInstances / instanceSet.size());
		return (double) noInstances / instanceSet.size();  
	}
	
	private double pSymbolicAttribute(SymbolicAttribute attribute, SymbolicValue attributeValue, SymbolicValue classValue) {
		int noClassInstances = 0;
		int noAttributeInstances = 0;
		
		for (Instance instance : instanceSet) {
			if(classValue.equals(instance.getValue(instance.noAttributes() - 1))) {
				noClassInstances++;
				if(attributeValue.equals(instance.getValue(attribute))) {
					noAttributeInstances++;
				}
			}
		}
		if(flagLaplace == true) {
			System.out.println();
//			System.out.println("Se foloseste corectia Laplace!");
//			System.out.println("No instante pt atribut " +  attribute.getName() + ": " + noAttributeInstances);
//			System.out.println("No instante pt clasa: " + noClassInstances);
			System.out.println("Probabilitate atribut " + attribute.getName() + ": " + (double)(noAttributeInstances + 1) / (noClassInstances + problemClass.noValues()));
//			System.out.println();
			return (double) (noAttributeInstances + 1) / (noClassInstances + problemClass.noValues());
		}
		else {
			return (double) noAttributeInstances / noClassInstances;
		}
	}
	
	private double pNumericAttribute(NumericAttribute attribute, NumericValue attributeValue, SymbolicValue classValue) {
		double intValue = attributeValue.getValue();
		double miu = calculateMiu(attribute, classValue);
		double delta = calculateDelta(attribute, classValue);
		double divide = 1 / (Math.sqrt(2*Math.PI) * delta);
		double exp = Math.exp(-1 * Math.pow((intValue - miu),2) / (2*Math.pow(delta, 2)));
		/*
		System.out.println("miu:" + miu);
		System.out.println("delta: " + delta);
		*/
		System.out.println("Probabilitate atribut " + attribute.getName() + ": " + divide * exp);
		System.out.println();
		
		return divide * exp; 
	}
	
	private double calculateMiu(NumericAttribute attribute, SymbolicValue classValue) {
		int noClassInstances = 0;
		double valuesSum = 0;
		
		for (Instance instance : instanceSet) {
			if(classValue.equals(instance.getValue(instance.noAttributes() - 1))) {
				noClassInstances++;
				NumericValue attributeValue = (NumericValue) instance.getValue(attribute);
				valuesSum += attributeValue.getValue(); 
			}
		}
		
		return valuesSum / noClassInstances; 
	}
	
	private double calculateDelta(NumericAttribute attribute, SymbolicValue classValue) {
		int noClassInstances = 0;
		double valuesSum = 0;
		
		for (Instance instance : instanceSet) {
			if(classValue.equals(instance.getValue(instance.noAttributes() - 1))) {
				noClassInstances++;
				NumericValue attributeValue = (NumericValue) instance.getValue(attribute);
				valuesSum += Math.pow(attributeValue.getValue() - calculateMiu(attribute, classValue), 2); 
			}
		}
		
		return Math.sqrt(valuesSum/noClassInstances);
	}

	public AttributeSet getAttributeSet() {
		return this.attributeSet;
	}
	
	public Vector<Instance> getInstanceSet() {
		return instanceSet;
	}
	
	public SymbolicAttribute getProblemClass() {
		return problemClass;
	}
	
	public void setFlagLaplace(boolean flagLaplace) {
		this.flagLaplace = flagLaplace;
	}

	public boolean isEmpty() {
		return instanceSet.size() <= 0;
	}
	
	double round(double d) {
		BigDecimal bd = new BigDecimal(Double.toString(d));
		bd = bd.setScale(3, BigDecimal.ROUND_HALF_UP);
		
		return bd.doubleValue();
	}

	public String toString() {
		String str = "";

		str += attributeSet + "\n";

		str += "Instances: \n";
		for (Instance instance : instanceSet) {
			str += instance;
		}

		return str;
	}
}
